package czopyk.model.structure;

import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

import model.Event;
import model.Time;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

import com.espertech.esper.collection.Pair;

import czopyk.model.analysis.Analyzer;
import czopyk.model.analysis.LoadReportEvent;
import czopyk.model.analysis.RequestCompleteEvent;
import czopyk.model.factories.Utils;
import czopyk.model.request.Request;

public class Simulator implements Observer {

	protected final czopyk.model.structure.Observable o = new czopyk.model.structure.Observable();

	protected final Logger log = Logger.getLogger(Simulator.class);

	private Map<Observable, Center> oc = new HashMap<Observable, Center>();
	private Map<Long, Double> registry = new HashMap<Long, Double>();
	private boolean[][] paths;
	private Map<Center, Integer> indexes = new HashMap<Center, Integer>();

	private Center gate;

	private Analyzer analyzer;

	// inicjalizacja macierzy istnienia sciezek
	// oraz posortowanie topologiczne
	private void initBarrierStructures() {
		List<Center> dfsQueue = new LinkedList<Center>();
		dfsQueue.add(gate);
		int i = 0;
		Center c;
		while (!dfsQueue.isEmpty()) {
			c = dfsQueue.remove(0);
			if (!indexes.containsKey(c)) {
				indexes.put(c, i++);
				for (Center next : c.getAllNext()) {
					dfsQueue.add(next);
				}
			}
		}

		// inicjalizacja tablicy sciezek
		int s = oc.size();
		paths = new boolean[s][s];
		for (boolean[] row : paths) {
			Arrays.fill(row, false);
		}

		// DFS - wypenienie tablicy sciezek
		List<Pair<Center, Center>> l = new LinkedList<Pair<Center, Center>>();
		for (Center target : gate.getAllNext()) {
			l.add(new Pair<Center, Center>(gate, target));
		}
		Pair<Center, Center> path;
		while (!l.isEmpty()) {
			path = l.remove(0);
			// jesli jeszcze nie ma polaczenia
			if (!paths[indexes.get(path.getFirst())][indexes.get(path.getSecond())]) {
				paths[indexes.get(path.getFirst())][indexes.get(path.getSecond())] = true;
				for (Center target : path.getSecond().getAllNext()) {
					// przedlurzenie sciezki
					l.add(new Pair<Center, Center>(path.getFirst(), target));
					// kolejne polaczenie bezposrednie
					l.add(new Pair<Center, Center>(path.getSecond(), target));
				}
			}
		}

	}

	// sprawdza, czy zadanie nalezy wposcic do kolejki c
	// jesli tak, to ustawia stosowny index na zadaniu
	public boolean blockRequest(Request r, Center c) {
		Set<Integer> rIds = r.getIndexes();
		int ic = indexes.get(c);

		for (Integer id : rIds) {
			if (paths[id][ic]) {
				// zadanie jest realizowane gdzies wczesniej i jest droga
				// blokada
				return true;
			}
		}

		rIds.add(ic);
		return false;
	}

	// przy wychodzeniu z centrum
	public void unlockIndex(Request r, Center c) {
		int ic = indexes.get(c);
		r.getIndexes().remove(ic);
	}

	public Double interval;

	public void setLoadReportInterval(Double i) {
		if (i == null) {
			log.warn("Load reporting interval set to null. Setting to default value 100.");
			i = 100.0;
		}
		if (i <= 0.0) {
			log.warn("Load reporting interval cannot be set to '" + i
					+ "'. Setting to default value 100.");
			i = 100.0;
		}
		interval = i;
	}

	private void initLoadReporting() {
		if (interval == null) {
			log.warn("Load reporting interval not set. Setting to default value 100.");
			interval = 100.0;
		}

		Time.registerEvent(new LoadMonitoringEvent(interval));
	}

	public void perform(Request completeRequest) {
		registry.put(completeRequest.getId(), Time.currentTime());
		log.debug("Received request - sending to gate...");
		gate.queue(completeRequest);
	}

	// zakonczono obsluge zgloszenia w pewnym centrum
	public void update(Observable obs, Object arg) {
		Center c = oc.get(obs);
		Request r = (Request) arg;

		// aktualizacja 'zrobienia'
		Utils.recursiveDone(r);

		// pchamy dalej, poki mozna
		if (!c.getAllNext().isEmpty()) {
			if (! (c instanceof LoadBalancer)) {
				for (Center cc : c.getAllNext()) {
					cc.queue(r.getParent());
				}
			} else {
				c.getNext().queue(r.getParent());
			}
		} else {
			if (r.isDone() && registry.containsKey(r.getId())) { // tylko calkowite
				double t = Time.currentTime() - registry.get(r.getId());
				registry.remove(r.getId());

				// do load generatora
				this.o.notifyObservers(r);

				// do analyzera
				this.o.notifyObservers(new RequestCompleteEvent(r.getLc(), t,
						Time.currentTime()));
			}
			// else zgoszenie zostalo rozbite na kilka i zostanie zakonczone gdzie
			// indziej
		}
	}

	private void register(Center center) {
		log.debug("Registring simulator as observer to center: " + center.getName());
		oc.put(center.o, center);
		center.setSimulator(this);
		center.o.addObserver(this);
		for (Center c : center.getAllNext()) {
			register(c);
		}
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("Simulator: gate: "
				+ (gate != null ? "set" : "unset") + "\n");
		sb.append(getString(gate, 0));
		return sb.toString();
	}

	private String getString(Center g, int i) {
		StringBuilder sb = new StringBuilder(StringUtils.repeat("\t", i) + g);
		if (g != null && g.getAllNext() != null) {
			for (Center c : g.getAllNext()) {
				sb.append(getString(c, i + 1));
			}
		}
		return sb.toString();
	}

	public void setAnalyzer(Analyzer a) {
		// rejestracja analyzera jako observera na wszystkich węzłach
		if (analyzer != null) {
			o.deleteObserver(analyzer);
		}
		/*
		 * if (gate != null) { gate.deleteObserverRecursively(analyzer);
		 * gate.addObserverRecursively(a); }
		 */
		analyzer = a;
		o.addObserver(a);
	}

	public void addObserver(Observer o) {
		this.o.addObserver(o);
	}

	public void setGate(Center gate) {
		this.gate = gate;
		oc.clear();
	}

	public void initSimulation() {
		indexes.clear();
		register(gate);
		initBarrierStructures();
		initLoadReporting();
	}
	
	public Center getGate() {
		return gate;
	}

	private class LoadMonitoringEvent extends Event {

		private final double interval; 
		
		public LoadMonitoringEvent(double interval) {
			super(Time.currentTime() + interval);
			this.interval = interval;
		}

		public void perform() {
			for (Center c : oc.values()) {
				double u = c.getUtilizationAndReset();
				o.notifyObservers(new LoadReportEvent(Math.max(u, 0.0), c.getName(),
						Time.currentTime()));
			}
			Time.registerEvent(new LoadMonitoringEvent(interval));
		}

		@Override
		public String getName() {
			return "LoadReportingEvent";
		}

	}

}
